2.3 接口与动态类型

接口多用于框架开发、第三方对接等场景,使用接口我们可以灵活的实现一些动态的功能场景。

本节我们将主要针对动态类型、接口内部实现机制、接口表与动态调用等内容进行讲解。

本节代码存放目录为 lesson3

静态类型与动态类型

静态、动态两种类型是Go语言中两个大的分类,而接口我们就会将其归纳到动态类型中。

静态类型

静态类型指的是:在代码编译后就不可改变的,也就是我们在写代码的时候就固定好的,是一直都不会变化的。

比如变量、常量等,这些在我们编译后就是固定的,是不会再改变的。例如:

var a int
var b string

这些变量在程序编译形成指令集机器码之后,就是固定运行的,不会产生任何变化,其实这也是静态语言的一大特性。


动态类型

动态类型指的就是:在代码编译后,程序运行的时候还可以动态变化,还可以变化为不同类型。

Go语言中,动态类型指的也就是interface{}。我们举例进行说明,代码如下:

type Animal interface {
    Speak()
}

type Dog struct {
    Name  string
    Voice string
}

func (d *Dog) Speak() {
    fmt.Printf("Name is %s, Speak-> %s\n", d.Name, d.Voice)
}

type Cat struct {
    Name  string
    Voice string
}

func (c *Cat) Speak() {
    fmt.Printf("Name is %s, Speak-> %s\n", c.Name, c.Voice)
}

var animalDog Animal = &Dog{
    Name:  "Dog",
    Voice: "Wang...",
}
animalDog.Speak()

var animalCat Animal = &Cat{
    Name:  "Dog",
    Voice: "Miaow...",
}
animalCat.Speak()

在上面的代码中,我们定义了一个接口Animal,同时也定义了两个结构体实现DogCat,我们可以看到Animal可以为Dog,也可以为Cat

那么在运行的时候,它也就可以属于不同的类型,他可以与结构体配合实现多态、也可以作为类型定义为intstring等。

接口的内部实现机制

接口其实也是一种通过底层结构组成的复合类型,通过底层的结构组合完成功能处理。下面我们将详细讲解接口的底层实现及调用。

接口的内部表示

Go语言中,接口一般具有两种用法。一种是空接口,用于接收一些不确定类型的值;另一种是非空接口,在接口中定义了一些方法。

不论是其中的哪一种,他们的结构基本是一样的,都具备类型信息、值信息。

类型信息(type):指向描述动态类型的具体信息,也就是指向了一个记录详细信息的地址。

值(data):保存实际的值或者指针。

基本结构基本与上面是一致的,其中空接口接口定义为eface,非空接口结构定义为iface


eface结构体(空接口)

空接口interface{}我们用于进行一些未知数据类型的接收,可以表示为任意类型。它的底层结构如下所示:

type eface struct {
    _type *typeInfo
    data  unsafe.Pointer
}
  • _type:指向存储该值类型信息的指针,即类型描述符。

  • data:指向实际存储的数据的指针。

在上面的结构中,具备了类型与值两个信息,简单来说就是底层结构体存储了实际的类型信息、实际值的指针地址。

进一步的,我们可以理解为,当我们使用类型断言的时候,其实就是访问了接口结构里面的type字段,而在取值的时候我们其实是访问了data字段,通过data指针获取到了实际的值。

我们可以通过下面的代码输出空接口的实际类型与值:

var (
    a interface{}
)
a = 1
fmt.Printf("类型为: %s\n", reflect.TypeOf(a))
fmt.Printf("值为: %v\n", reflect.ValueOf(a))

在上面的代码中,使用了reflect.TypeOf(a)获取实际的类型信息,使用reflect.ValueOf(a)获取了实际的值。

我们进一步分析一下TypeOfValueOf,源码如下所示:

func TypeOf(i any) Type {
    eface := *(*emptyInterface)(unsafe.Pointer(&i))
    return toType(eface.typ)
}

func toType(t *rtype) Type {
    if t == nil {
        return nil
    }
    return t
}

func ValueOf(i any) Value {
    if i == nil {
        return Value{}
    }

    // TODO: Maybe allow contents of a Value to live on the stack.
    // For now we make the contents always escape to the heap. It
    // makes life easier in a few places (see chanrecv/mapassign
    // comment below).
    escapes(i)

    return unpackEface(i)
}

// unpackEface converts the empty interface i to a Value.
func unpackEface(i any) Value {
    e := (*emptyInterface)(unsafe.Pointer(&i))
    // NOTE: don't read e.word until we know whether it is really a pointer or not.
    t := e.typ
    if t == nil {
        return Value{}
    }
    f := flag(t.Kind())
    if ifaceIndir(t) {
        f |= flagIndir
    }
    return Value{t, e.word, f}
}

// emptyInterface is the header for an interface{} value.
type emptyInterface struct {
    typ  *rtype
    word unsafe.Pointer
}

我们可以仔细分析一下上面的源码,我们就可以看到通过eface := *(*emptyInterface)(unsafe.Pointer(&i))进行转换,在emptyInterface中就包含了typeword两个信息。

TypeOf实现中,也是直接通过eface.typ获取了实际的类型信息。


iface结构体(非空接口)

非空接口也就是上文我们例子中的Animal这样的,接口具有方法集。它的结构如下所示:

type iface struct {
    tab  *itab
    data unsafe.Pointer
}
  • tab:指向itab结构体的指针,itab包含接口类型和动态类型之间的关联信息,以及方法的具体实现指针。

  • data:与eface类似,指向实际数据的指针。

非空接口的结构与空接口的结构大致是差不多的,不同的是:空接口存type信息,而非空接口存储了一个tab信息。

itab结构在非空接口的实现中起着关键作用,它保存了接口类型与具体类型之间的关系以及实现该接口的方法指针。结构如下所示:

type itab struct {
    inter  *interfacetype  // 接口类型的信息
    _type  *_type          // 动态类型的信息,描述具体类型的结构
    fun    [1]uintptr      // 方法表,存储实际的方法实现指针
}

inter 主要存储该接口本身的类型信息。这个字段描述了接口的类型,包括接口定义的方法集,这是接口的静态类型信息。按照上文的例子来说,就是描述了Animal的具体信息,包括Speak等方法信息。

_type 则是指向具体实现接口的类型信息,即接口的动态类型,主要描述了实现接口的具体类型的结构。也就是例子中的DogCat等结构体的类型信息。

fun [1]uintptr 是一个数组,用来存储指向方法实现的函数指针。数组中的每个元素对应接口方法集中的一个方法。fun数组的大小取决于接口定义的方法数,每个元素都是一个方法的入口地址。比如例子中的Dog Speak就会存储到fun数组中。

动态调用过程

接口的机制其实挺复杂的,调用过程同样是与接口进行绑定的。关于接口的动态调用过程,我们将以一个例子来进行说明。

var animalDog Animal = &Dog{
    Name:  "Dog",
    Voice: "Wang...",
}
animalDog.Speak()

我们主要搞清楚一个问题:上述代码调用都发生了什么?

编译时

  • 接口类型确定:编译器确定接口Animal的类型信息,包括Animal方法集Speak()方法,这些信息最终存储到inter中。

  • 具体类型确定:编译器确定具体实现Dog的类型信息,包括实现的方法Speak,这些信息最终存储到_type中。

  • 生成方法表:编译器Dog的实现方法Speak()存放到了fun中。

上面的这些信息并不会立即就生成结构itab,而是在运行时才会初始化生成,所以说在编译时仅仅只是确定了这些静态信息。


运行时

  • 生成接口实例:运行时将根据编译静态信息生成iface结构,也就是生成animalDog的结构信息。

  • 处理itab:通过编译形成的信息生成itab,最终组成完整的iface结构。

  • 执行:在生成结构以后,当调用animalDog.Speak()时就会去查找itab结构,找到之后再次查找方法集合,最终找到方法集合后调用执行。

总的来说,其实主要就是分为三个步骤。

  • 第一步骤:编译时确定相关的类型信息及方法。

  • 第二步骤:运行时创建结构信息,生成接口表。

  • 第三步骤:调用函数时查找方法表,找到之后调用执行。

小结

本节我们讲解了接口的内部机制及动态调用过程。这一块逻辑比较复杂,一般来说我们不需要特别的去深入,只需要知道怎么去使用它基本就可以了。

关于本节总结如下:

  • 静态类型是编译后就不可变的

  • 动态类型是编译后可动态生成的

  • 接口的底层类型包含typedata

  • 非空接口通过接口表的结构实现方法的调用处理

results matching ""

    No results matching ""